package nkr1pt.popkorn.download;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
import nkr1pt.popkorn.events.DownloadEvent;
import nkr1pt.popkorn.events.EventBus;
import nkr1pt.popkorn.events.EventObserver;
import nkr1pt.popkorn.events.PopkornEvent;
import java.io.File;
import java.io.FileFilter;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import nkr1pt.popkorn.events.PopkornEvent.EventType;
import nkr1pt.popkorn.events.PopkornEventFactory;
import nkr1pt.popkorn.extract.Extractor;
import nkr1pt.popkorn.extract.ExtractorProvider;
import nkr1pt.popkorn.helper.FileUtils;
import nkr1pt.popkorn.helper.NumberUtils;
import nkr1pt.popkorn.properties.PropertyLoader;
import nkr1pt.popkorn.schedule.SchedulerMode;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.Validate;

/**
 *
 * @author Kristof Vanhaeren
 */
@Singleton
public class DownloadManager implements EventObserver {

    private ExecutorService execService = Executors.newFixedThreadPool(4);
    private CopyOnWriteArrayList<DownloadCandidate> downloadCandidates = new CopyOnWriteArrayList<DownloadCandidate>();
    private EventBus eventBus;
    private ExtractorProvider extractorProvider;
    private DownloaderProvider downloaderProvider;
    private DownloadCandidateValidatorProvider validatorProvider;
    private PropertyLoader propertyLoader;
    private DownloadCandidateFactory downloadCandidateFactory;
    private PopkornEventFactory eventFactory;
    private DownloadCandidateValidator validatorStrategy;
    private SchedulerMode schedulerMode;
    private static final Logger logger = Logger.getLogger(DownloadManager.class.getName());

    private Map<File, Integer> downloadCandidateExtractionCountMap = new HashMap<File, Integer>();

    @Inject
    public DownloadManager(EventBus eventBus,
            DownloadCandidateFactory downloadCandidateFactory,
            ExtractorProvider extractorProvider,
            DownloaderProvider downloaderProvider,
            DownloadCandidateValidatorProvider validatorProvider,
            PropertyLoader propertyLoader,
            PopkornEventFactory eventFactory) {
        this.eventBus = eventBus;
        this.extractorProvider = extractorProvider;
        this.validatorProvider = validatorProvider;
        this.downloaderProvider = downloaderProvider;
        this.propertyLoader = propertyLoader;
        this.downloadCandidateFactory = downloadCandidateFactory;
        this.eventFactory = eventFactory;

        Set<PopkornEvent.EventType> eventSet = new HashSet<PopkornEvent.EventType>();
        eventSet.add(PopkornEvent.EventType.DOWNLOAD_STARTED);
        eventSet.add(PopkornEvent.EventType.DOWNLOAD_COMPLETED);
        eventSet.add(PopkornEvent.EventType.DOWNLOAD_FAILED);
        eventSet.add(PopkornEvent.EventType.EXTRACTION_FAILED);
        eventSet.add(PopkornEvent.EventType.STATUS);
        eventSet.add(PopkornEvent.EventType.DOWNLOADCANDIDATE_CREATED);
        eventSet.add(PopkornEvent.EventType.DOWNLOADCANDIDATE_DELETED);
        eventSet.add(PopkornEvent.EventType.SCHEDULE_IMMEDATE);
        eventSet.add(PopkornEvent.EventType.SCHEDULE_NIGHT);
        eventBus.subscribe(this, eventSet);
    }

    public void startDownloading() {
        downloadCandidates.addAll(prepareDownloadCandidates());

        for (final DownloadCandidate candidate : downloadCandidates) {
            if (!candidate.isEligibleToDownload()) {
                continue;
            }

            candidate.setState(DownloadCandidateState.INPROGRESS);
            candidate.setCurrentLocation(propertyLoader.getProperties().getDownloadInProgress());
            logger.log(Level.INFO, "Starting downloading all parts of {0} (size = {1} MB)", new Object[]{candidate.getName(), NumberUtils.bytesToMegaBytes(candidate.getSize())});
            for (final DownloadCandidateItem item : candidate.getItems()) {
                execService.execute(new Runnable() {

                    public void run() {
                        if (item.getState().equals(DownloadCandidateState.WAITING)) {
                            Downloader downloader = downloaderProvider.get();
                            downloader.download(item);
                        }
                    }
                });

            }
        }

    }

    public List<DownloadCandidate> prepareDownloadCandidates() {
        List<DownloadCandidate> candidates = new ArrayList<DownloadCandidate>();

        File newDownloadsDir = new File(propertyLoader.getProperties().getDownloadCandidates());
        toDownloadLoop:
        for (File file : newDownloadsDir.listFiles(new ValidPopkornFileFilter())) {
            String name = FilenameUtils.removeExtension(FilenameUtils.getName(file.getName()));
            for (DownloadCandidate c : downloadCandidates) {
                if (c.getName().equals(name)) {
                    continue toDownloadLoop;
                }
            }
            DownloadCandidate candidate = createDownloadCandidate(file);
            if (candidate.isInvalid()) {
                handleDownloadFailedEvent(eventFactory.newEvent(EventType.DOWNLOAD_FAILED, candidate));
            } else {
                candidates.add(candidate);
            }
        }

        File inProgressDir = new File(propertyLoader.getProperties().getDownloadInProgress());
        inProgressLoop:
        for (File dir : inProgressDir.listFiles()) {
            if (dir.isDirectory()) {
                for (File file : dir.listFiles(new ValidPopkornFileFilter())) {
                    String name = FilenameUtils.removeExtension(FilenameUtils.getName(file.getName()));
                    for (DownloadCandidate c : downloadCandidates) {
                        if (c.getName().equals(name)) {
                            continue inProgressLoop;
                        }
                    }
                    DownloadCandidate candidate = createDownloadCandidate(file);
                    if (candidate.isInvalid()) {
                        handleDownloadFailedEvent(eventFactory.newEvent(EventType.DOWNLOAD_FAILED, candidate));
                    } else {
                        candidates.add(candidate);
                    }
                }
            }
        }

        return candidates;
    }

    private DownloadCandidate createDownloadCandidate(File file) {
        DownloadCandidate candidate = downloadCandidateFactory.createDownloadCandidate(file);
        if (candidate.isInvalid()) {
            return candidate;
        }

        validatorProvider.setHost(candidate.getHost());
        validatorStrategy = validatorProvider.get();
        validatorStrategy.validate(candidate);
        return candidate;
    }

    public CopyOnWriteArrayList<DownloadCandidate> getDownloadCandidates() {
        return downloadCandidates;
    }

    public void handleEvent(PopkornEvent event) {
        switch (event.getType()) {
            case DOWNLOAD_STARTED:
                handleDownloadStartedEvent((DownloadEvent) event);
                break;
            case DOWNLOAD_COMPLETED:
                handleDownloadCompletedEvent((DownloadEvent) event);
                break;
            case DOWNLOAD_FAILED:
                handleDownloadFailedEvent((DownloadEvent) event);
                break;
            case EXTRACTION_COMPLETED:
                handleExtractionCompletedEvent((DownloadEvent) event);
                break;
            case EXTRACTION_FAILED:
                handleExtractionFailedEvent((DownloadEvent) event);
                break;
            case DOWNLOADCANDIDATE_CREATED:
                handleDownloadCandidateCreatedEvent((DownloadEvent) event);
                break;
            case DOWNLOADCANDIDATE_DELETED:
                handleDownloadCandidateDeletedEvent((DownloadEvent) event);
                break;
            case DOWNLOADCANDIDATE_MODIFIED:
                handleDownloadCandidateModifiedEvent((DownloadEvent) event);
                break;
            case SCHEDULE_IMMEDATE:
                schedulerMode = SchedulerMode.IMMEDIATELY;
                break;
            case SCHEDULE_NIGHT:
                schedulerMode = SchedulerMode.NIGHTLY;
                break;
        }
    }

    private void handleDownloadStartedEvent(DownloadEvent event) {
        DownloadCandidateItem item = event.getDownloadCandidateItem();
        item.setState(DownloadCandidateState.INPROGRESS);
        logger.log(Level.INFO, "Download started: {0} (size = {1} MB)", new Object[]{event.getDownloadCandidateItem().getName(), NumberUtils.bytesToMegaBytes(item.getSize())});
    }

    private void handleDownloadCompletedEvent(DownloadEvent event) {
        event.getDownloadCandidateItem().setState(DownloadCandidateState.COMPLETE);
        logger.log(Level.INFO, "Download completed: {0}", event.getDownloadCandidateItem().getName());
        logger.log(Level.INFO, "Progress for {0}: {1} %", new Object[]{event.getDownloadCandidate().getName(), event.getDownloadCandidate().getProgress()});
        final DownloadCandidate downloadCandidate = event.getDownloadCandidateItem().getDownloadCandidate();
        if (downloadCandidate.isComplete()) {
            logger.log(Level.INFO, "Download completed: {0}", event.getDownloadCandidate().getName());
            downloadCandidate.setCurrentLocation(propertyLoader.getProperties().getDownloadCompleted());
            if (downloadCandidate.isExtractable()) {
                extract(downloadCandidate);
            }
        }
    }

    private void handleDownloadFailedEvent(DownloadEvent event) {
        Validate.notNull(event.getDownloadCandidate());

        DownloadCandidate candidate = event.getDownloadCandidate();
        candidate.setState(DownloadCandidateState.FAILED);
        FileUtils.moveFileToDirectory(candidate.getFile(), new File(propertyLoader.getProperties().getDownloadFailed()));
        logger.log(Level.INFO, "Download failed: {0}", candidate.getName());
        if (event.getException() != null) {
            logger.log(Level.SEVERE, event.getException().getMessage(), event.getException());
        }
    }

    private void handleExtractionCompletedEvent(DownloadEvent extractionCompletedEvent) {
        DownloadCandidate downloadCandidate = (DownloadCandidate) extractionCompletedEvent.getDownloadCandidate();
        downloadCandidates.remove(downloadCandidate);
    }

    private void handleExtractionFailedEvent(DownloadEvent extractionFailedEvent) {
        DownloadCandidate downloadCandidate = (DownloadCandidate) extractionFailedEvent.getDownloadCandidate();
        downloadCandidate.setState(DownloadCandidateState.INPROGRESS);
        downloadCandidate.setCurrentLocation(downloadCandidate.getInProgressLocation());

        int extractCounter = 0;
        if (downloadCandidateExtractionCountMap.get(downloadCandidate.getFile()) != null) {
            extractCounter = downloadCandidateExtractionCountMap.get(downloadCandidate.getFile());
        }
        extractCounter++;
        downloadCandidateExtractionCountMap.put(downloadCandidate.getFile(), extractCounter);
        if (extractCounter >= 2) {
            downloadCandidateExtractionCountMap.remove(downloadCandidate.getFile());
            downloadCandidate.setState(DownloadCandidateState.EXTRACTION_FAILED);
            downloadCandidate.setCurrentLocation(downloadCandidate.getExtractionFailedLocation());
        } else {
            downloadCandidate.removeItems(downloadCandidate.getCorruptedItems());
            startDownloading();
        }
    }

    private void handleDownloadCandidateCreatedEvent(DownloadEvent downloadCandidateCreatedEvent) {
        if (schedulerMode.equals(SchedulerMode.IMMEDIATELY)) {
            logger.log(Level.INFO, "{0} added to the downloading queue.", downloadCandidateCreatedEvent.getFile().getName());
            startDownloading();
        }
    }

    private void handleDownloadCandidateDeletedEvent(DownloadEvent downloadCandidateDeletedEvent) {
        File file = downloadCandidateDeletedEvent.getFile();
        for (DownloadCandidate downloadCandidate : downloadCandidates) {
            if (downloadCandidate.getFile().equals(file)) {
                if (downloadCandidate.getState().equals(DownloadCandidateState.WAITING)) {
                    downloadCandidates.remove(downloadCandidate);
                    logger.log(Level.INFO, "{0} removed from the downloading queue.", downloadCandidate.getName());
                }
            }
        }
    }

    private void handleDownloadCandidateModifiedEvent(DownloadEvent downloadCandidateModifiedEvent) {
        File file = downloadCandidateModifiedEvent.getFile();
        for (DownloadCandidate downloadCandidate : downloadCandidates) {
            if (downloadCandidate.getFile().equals(file)) {
                if (downloadCandidate.getState().equals(DownloadCandidateState.WAITING)) {
                    downloadCandidates.remove(downloadCandidate);
                    createDownloadCandidate(file);
                    return;
                } else {
                    logger.log(Level.INFO, "modification of {0} ignored because it is currently being downloaded.", downloadCandidate.getName());
                }
            }
        }
    }

    private void extract(final DownloadCandidate downloadCandidate) {
        Thread extractThread = new Thread(new Runnable() {

            public void run() {
                Extractor extractor = extractorProvider.get();
                extractor.extract(downloadCandidate);
            }
        });
        extractThread.start();
    }

    private class ValidPopkornFileFilter implements FileFilter {

        public boolean accept(File pathname) {
            if (pathname.isFile()
                    && !pathname.isDirectory()
                    && (FileUtils.getExtension(pathname).equals("txt")
                    || FileUtils.getExtension(pathname).equals(""))) {
                return true;
            }

            return false;
        }
    }
}